/*
 * Copyright (c) 2018, apexes.net. All rights reserved.
 *
 *         http://www.apexes.net
 *
 */
package net.apexes.commons.lang;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.URL;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeoutException;

/**
 * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
 */
public final class Networks {
    private Networks() {
    }

    /**
     * 获取网络时间戳
     * @return 返回网络时间戳
     */
    public static long networkTimeMillis()
            throws NetworkTimeMillisException, InterruptedException, ExecutionException, TimeoutException {
        return networkTimeMillis(
                new InetSocketAddress("pool.ntp.org", NTPTimeMillis.DEFAULT_PORT),
                new InetSocketAddress("ntp.aliyun.com", NTPTimeMillis.DEFAULT_PORT),
                new InetSocketAddress("time.windows.com", NTPTimeMillis.DEFAULT_PORT),
                new InetSocketAddress("time.apple.com", NTPTimeMillis.DEFAULT_PORT));
    }

    /**
     * 从指定的网络地址获取网络时间戳
     * @param addrs NTP服务器地址
     * @return 返回网络时间戳结果列表
     */
    public static long networkTimeMillis(InetSocketAddress... addrs)
            throws NetworkTimeMillisException, InterruptedException, ExecutionException, TimeoutException {
        return networkTimeMillis(NTPTimeMillis.DEFAULT_TIMEOUT_MS, addrs);
    }

    /**
     * 从指定的网络地址获取网络时间戳
     * @param executor 执行获取时间任务的线程池
     * @param addrs NTP服务器地址
     * @return 返回网络时间戳
     */
    public static long networkTimeMillis(ExecutorService executor, InetSocketAddress... addrs)
            throws NetworkTimeMillisException, InterruptedException, ExecutionException, TimeoutException {
        return networkTimeMillis(executor, NTPTimeMillis.DEFAULT_TIMEOUT_MS, addrs);
    }

    /**
     * 从指定的网络地址获取网络时间戳
     * @param timeoutMs 超时时间，单位ms
     * @param addrs NTP服务器地址
     * @return 返回网络时间戳
     */
    public static long networkTimeMillis(int timeoutMs, InetSocketAddress... addrs)
            throws NetworkTimeMillisException, InterruptedException, ExecutionException, TimeoutException {
        return NetworkTimeMillis.invoke(timeoutMs, addrs);
    }

    /**
     * 从指定的网络地址获取网络时间戳
     * @param executor 执行获取时间任务的线程池
     * @param timeoutMs 超时时间，单位ms
     * @param addrs NTP服务器地址
     * @return 返回网络时间戳
     */
    public static long networkTimeMillis(ExecutorService executor, int timeoutMs, InetSocketAddress... addrs)
            throws NetworkTimeMillisException, InterruptedException, ExecutionException, TimeoutException {
        return NetworkTimeMillis.invoke(executor, timeoutMs, addrs);
    }

    /**
     * 判断指定的异常是否是网络未连接上产生的异常
     * @param throwable 要检查的异常
     * @return 如果是网络未连接产生的异常就返回 true
     */
    public static boolean isNotConnectThrowable(Throwable throwable) {
        if (throwable == null) {
            return false;
        }
        if (throwable instanceof java.net.NoRouteToHostException
                || throwable instanceof java.net.UnknownHostException
                || throwable instanceof javax.net.ssl.SSLHandshakeException
                || throwable instanceof java.net.ConnectException) {
            return true;
        }
        Throwable cause = throwable.getCause();
        if (cause != null) {
            return isNotConnectThrowable(cause);
        }
        return false;
    }

    /**
     * 返回 {@code application/x-www-form-urlencoded} 字符串，如 key1=value1&key2=value2
     */
    public static String forNameValuePair(Iterable<Map.Entry<String, String>> iterable) {
        try {
            return forNameValuePair(iterable, StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e) {
            throw Exceptions.rethrow(e);
        }
    }

    /**
     * 返回 {@code application/x-www-form-urlencoded} 字符串，如 key1=value1&key2=value2
     */
    public static String forNameValuePair(Iterable<Map.Entry<String, String>> iterable, String charset)
            throws UnsupportedEncodingException {
        return forNameValuePair(iterable, Charset.forName(charset));
    }

    public static String forNameValuePair(Iterable<Map.Entry<String, String>> iterable, Charset charset)
            throws UnsupportedEncodingException {
        Checks.verifyNotNull(iterable, "iterable");
        Checks.verifyNotNull(charset, "charset");
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> entry : iterable) {
            if (entry.getValue() != null) {
                String key = URLEncoder.encode(entry.getKey(), charset.name());
                String value = URLEncoder.encode(entry.getValue(), charset.name());
                if (sb.length() > 0) {
                    sb.append('&');
                }
                sb.append(key);
                sb.append('=');
                sb.append(value);
            }
        }
        return sb.toString();
    }

    /**
     * 获取本机的MAC地址的字符串格式。xx:xx:xx:xx
     */
    public static String macString() {
        byte[] macBytes = macBytes();
        if (macBytes == null) {
            return null;
        }
        StringBuilder buf = new StringBuilder();
        for (byte b : macBytes) {
            buf.append(Bytes.toHex(b));
            buf.append(":");
        }
        buf.setLength(buf.length() - 1);
        return buf.toString();
    }

    /**
     * 获取本机的MAC地址的Long值
     */
    public static Long macValue() {
        byte[] macBytes = macBytes();
        if (macBytes == null) {
            return null;
        }
        byte[] bytes = new byte[8];
        System.arraycopy(macBytes, 0, bytes, 2, 6);
        return Bytes.bytesToLong(bytes);
    }

    public static byte[] macBytes() {
        try {
            Enumeration<NetworkInterface> nics = NetworkInterface.getNetworkInterfaces();
            if (nics == null) {
                return null;
            }
            while (nics.hasMoreElements()) {
                NetworkInterface ifc = nics.nextElement();
                if (!ifc.isLoopback()) {
                    byte[] data = ifc.getHardwareAddress();
                    if (data != null && data.length == 6) {
                        return data;
                    }
                }
            }
        } catch (java.net.SocketException e) {
            // fine, let's take is as signal of not having any interfaces
        }
        return null;
    }

    public static InetAddress local() throws SocketException, UnknownHostException {
        InetAddress candidateAddress = null;
        Enumeration<NetworkInterface> enumeration = NetworkInterface.getNetworkInterfaces();
        while (enumeration.hasMoreElements()) {
            NetworkInterface netIf = enumeration.nextElement();
            for (Enumeration<InetAddress> inetAddress = netIf.getInetAddresses(); inetAddress.hasMoreElements();) {
                InetAddress next = inetAddress.nextElement();
                // 排除回环类型地址
                if (!next.isLoopbackAddress()) {
                    if (next.isSiteLocalAddress()) {
                        return next;
                    }
                    // 若不是site-local地址，记录下该地址当作候选
                    if (candidateAddress == null) {
                        candidateAddress = next;
                    }
                }
            }
        }
        return candidateAddress == null ? InetAddress.getLocalHost() : candidateAddress;
    }

    public static Pair<InetAddress, NetworkInterface> findSiteLocal() throws SocketException {
        Enumeration<NetworkInterface> enumeration = NetworkInterface.getNetworkInterfaces();
        while (enumeration.hasMoreElements()) {
            NetworkInterface netIf = enumeration.nextElement();
            for (Enumeration<InetAddress> inetAddress = netIf.getInetAddresses(); inetAddress.hasMoreElements();) {
                InetAddress next = inetAddress.nextElement();
                // 排除回环类型地址
                if (next.isLoopbackAddress()) {
                    continue;
                }
                if (next.isSiteLocalAddress() && next instanceof Inet4Address) {
                    return new Pair<>(next, netIf);
                }
            }
        }
        return null;
    }

    public static String url(String host, String path) {
        if (host.endsWith("/")) {
            if (path.startsWith("/")) {
                return host + path.substring(1);
            }
        } else if (!path.startsWith("/")) {
            return host + "/" + path;
        }
        return host + path;
    }

    public static HttpURLConnection connect(URL url, SSLSocketFactory socketFactory, HostnameVerifier hostnameVerifier)
            throws IOException {
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        if (conn instanceof HttpsURLConnection) {
            HttpsURLConnection https = (HttpsURLConnection) conn;
            if (hostnameVerifier != null) {
                https.setHostnameVerifier(hostnameVerifier);
            }
            if (socketFactory != null) {
                https.setSSLSocketFactory(socketFactory);
            }
        }
        return conn;
    }

    public static HttpURLConnection connectExemptSSL(URL url) throws IOException, KeyManagementException, NoSuchAlgorithmException {
        if ("https".equalsIgnoreCase(url.getProtocol())) {
            return connect(url, exemptSSLContext().getSocketFactory(), exemptHostnameVerifier());
        }
        return connect(url, null, null);
    }

    /**
     * 创建一个支持双向认证的SSLContext
     */
    public static SSLContext mutualAuthSSLContext(String trustCert, String keyCert, String keyCertPassword)
            throws Exception {
        KeyManager[] keyManagers = certKeyManagers(keyCert, keyCertPassword);
        TrustManager[] trustManagers = certTrustManagers(trustCert);
        SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
        sslContext.init(keyManagers, trustManagers, new SecureRandom());
        return sslContext;
    }

    /**
     * 创建一个不检查证书的SSLContext
     *
     * @return 返回一个不检查证书的SSLContext实例
     */
    public static SSLContext exemptSSLContext() throws NoSuchAlgorithmException, KeyManagementException {
        SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
        sslContext.init(null, exemptTrustManagers(), new SecureRandom());
        return sslContext;
    }

    /**
     * 创建一个不验证的HostnameVerifier
     * @return 返回一个不验证的HostnameVerifier实例
     */
    public static HostnameVerifier exemptHostnameVerifier() {
        return exemptHostnameVerifier;
    }

    /**
     * 使用base64格式的证书创建KeyManager数组
     */
    public static KeyManager[] certKeyManagers(String pemCert, String certPassword) throws Exception {
        Checks.verifyNotNull(pemCert, "pemCert");
        Checks.verifyNotNull(certPassword, "certPassword");
        byte[] bytes = Base64.decode(pemCert);
        assert bytes != null;
        KeyStore keySotre = KeyStore.getInstance("PKCS12");
        keySotre.load(new ByteArrayInputStream(bytes), certPassword.toCharArray());
        KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
        kmf.init(keySotre, certPassword.toCharArray());
        return kmf.getKeyManagers();
    }

    /**
     * 使用base64格式的证书创建TrustManager数组
     */
    public static TrustManager[] certTrustManagers(String pemCert) throws Exception {
        Checks.verifyNotNull(pemCert, "pemCert");
        byte[] bytes = Base64.decode(pemCert);
        assert bytes != null;
        CertificateFactory factory = CertificateFactory.getInstance("X.509");
        X509Certificate cert = (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(bytes));
        String alias = cert.getSubjectX500Principal().getName();
        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        trustStore.load(null);
        trustStore.setCertificateEntry(alias, cert);
        TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
        tmf.init(trustStore);
        return tmf.getTrustManagers();
    }

    /**
     * 创建一个不检查证书的TrustManager[]
     */
    public static TrustManager[] exemptTrustManagers() {
        return ExemptVerifyTrustManager.trustManagers;
    }

    /**
     * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
     */
    private static class ExemptVerifyTrustManager implements X509TrustManager {
        static TrustManager[] trustManagers = new TrustManager[]{new ExemptVerifyTrustManager()};
        private static final X509Certificate[] _X509S = new X509Certificate[]{};

        @Override
        public void checkClientTrusted(X509Certificate[] x509s, String s) {
        }

        @Override
        public void checkServerTrusted(X509Certificate[] x509s, String s) {
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return _X509S;
        }
    }

    /**
     * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
     */
    private static final HostnameVerifier exemptHostnameVerifier = new HostnameVerifier() {
        @Override
        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    };

}
